/**
 * This class is the main runner code which defines the machine comprising of the clients and the server on every machine. 
 * For more explanation see the packages names and class definitions. 
 */

/**
 * @author arpitagarwal
 * Copyright 2012, held by Arpit Agarwal, of University of Illinois at Urbana-Champaign. 
 */

package runner;

import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream.GetField;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.Hashtable;

import server.MultiThreadedServer;
import server.WorkerRunnable;
import client.Client;

public class RunSystem {

	private Hashtable<Integer, String> hashtable = new Hashtable<Integer, String>();
	private static final int PORT = 20000;
	private static String IPAddressLeft; // = "localhost";
	private static String IPAddressRight; // = "localhost";
	private static String FILE_HASHTABLE;
	private static String FILE_LIST_MACHINES_OF_DISSYS = "/home/aagarwl8/workspace/DisMyRepo/disSource/runner/";
	private static final int TIME_BW_HEARTBEATS = 10;
	public static final int MIN_TIME_BW_PINGS = 3000;
	public static final int MIN_RECONNECT_TIME = 2000; 
	public static int NUM_MACHINES = 4; 
	public static String MACHINE_NAME;
	public int reconnectRequests;
	ArrayList<String> tempListMachines; 
	
	private long previousReconnectRequest; 

	Client leftClient;
	Client rightClient;
	// private boolean isModifyingWorkerList;

	private boolean isLeftDead;
	private boolean isRightDead;
	long previousTimeRight;
	long previousTimeLeft;

	private long previousMessageSendTime;

	private int getServerPort() {
		return PORT;
	}

	private String getIPAddressLeft() {
		return IPAddressLeft;
	}

	private String getIPAddressRight() {
		return IPAddressRight;
	}

	private void setIPAddressLeft(String iPAddressLeft) {
		IPAddressLeft = iPAddressLeft;
	}

	private void setIPAddressRight(String iPAddressRight) {
		IPAddressRight = iPAddressRight;
	}

	public static final void systemWait(long args) {
		try {
			Thread.sleep(args);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public void loadHashtableFromFile(String filename) {
		try {
			String line;
			FileInputStream inFile = new FileInputStream(filename);
			DataInputStream data = new DataInputStream(inFile);
			BufferedReader br = new BufferedReader(new InputStreamReader(data));
			while ((line = br.readLine()) != null) {
				hashtable.put(Integer.parseInt(line.split("-")[0]),
						line.split("-")[1]);
			}
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	public synchronized static void main(String[] args) {

		FILE_LIST_MACHINES_OF_DISSYS += args[0];
		MultiThreadedServer.print("Machine file name: "
				+ FILE_LIST_MACHINES_OF_DISSYS);

		MACHINE_NAME = args[0].replace("listMachines", "Dictionary");

		MultiThreadedServer.print("Machine file name: " + MACHINE_NAME);

		MultiThreadedServer serverObject = new MultiThreadedServer(PORT);
		RunSystem objectRunner = new RunSystem();
		objectRunner.previousMessageSendTime = 0;
		
		objectRunner.tempListMachines = objectRunner
				.getListMachinesDisSys(FILE_LIST_MACHINES_OF_DISSYS);

		NUM_MACHINES = objectRunner.tempListMachines.size() + 1; 
		
		// objectRunner.isModifyingWorkerList = false;

		Thread serverThread = new Thread(serverObject);
		serverThread.start();
		
		FILE_HASHTABLE = MACHINE_NAME; 
		
		String filename = FILE_HASHTABLE;
		
//		objectRunner.loadHashtableFromFile(filename);

		systemWait(6000);

		objectRunner.previousReconnectRequest = 0;  
		objectRunner.reconnectRequests = 0; 
		
		objectRunner
				.setIPAddresses(objectRunner.tempListMachines.get(0).split("-")[0],
						objectRunner.tempListMachines.get(objectRunner.tempListMachines.size() - 1)
								.split("-")[0]);

		// objectRunner.setIPAddresses("localhost", "localhost");
		// objectRunner.setIPAddresses("192.17.11.207", "192.17.11.209");
		// objectRunner.setIPAddresses("192.17.11.208", "192.17.11.206");
		// objectRunner.setIPAddresses("192.17.11.209", "192.17.11.207");
		// objectRunner.setIPAddresses("192.17.11.206", "192.17.11.208");

		objectRunner.leftClient = new Client(objectRunner.getIPAddressLeft(),
				objectRunner.getServerPort());
		objectRunner.rightClient = new Client(objectRunner.getIPAddressRight(),
				objectRunner.getServerPort());
		objectRunner.previousTimeLeft = System.currentTimeMillis();
		objectRunner.previousTimeRight = System.currentTimeMillis();
		while (true) {
			objectRunner.sendAliveMessage();
			synchronized (serverObject.workerList) {
				try {
					for (WorkerRunnable workerObject : serverObject.workerList) {
						
						objectRunner.reconnectRequests = 0;
						
						String messageReceived = workerObject
								.getMessageReceived();
						if (messageReceived.length() < 5) {
							continue;
						}

						long time = System.currentTimeMillis();

						if (messageReceived.split(":")[0]
								.equalsIgnoreCase("left")) {
							objectRunner.previousTimeLeft = System
									.currentTimeMillis();
							if (!messageReceived.split(":")[1]
									.equalsIgnoreCase("ping")
									&& !messageReceived.split(":")[1]
											.equalsIgnoreCase("insert")) {
								MultiThreadedServer
										.print("---->Message received: "
												+ messageReceived
												+ ", from "
												+ objectRunner.leftClient
														.getIpAddress()
												+ ":"
												+ objectRunner.leftClient
														.getPort());
							}
						} else if (messageReceived.split(":")[0]
								.equalsIgnoreCase("right")) {
							objectRunner.previousTimeRight = System
									.currentTimeMillis();
							if (!messageReceived.split(":")[1]
									.equalsIgnoreCase("ping")
									&& !messageReceived.split(":")[1]
											.equalsIgnoreCase("insert")) {
								MultiThreadedServer
										.print("---->Message received: "
												+ messageReceived
												+ ", from "
												+ objectRunner.rightClient
														.getIpAddress()
												+ ":"
												+ objectRunner.rightClient
														.getPort());
							}
						}

						String deadMachine = objectRunner.isAnybodyDead();

						if (!deadMachine.equals("none")) {
							if (deadMachine.equalsIgnoreCase("right")) {
								objectRunner.isLeftDead = false;
								objectRunner.isRightDead = true;
							} else if (deadMachine.equalsIgnoreCase("left")) {
								objectRunner.isLeftDead = true;
								objectRunner.isRightDead = false;
							}
//							MultiThreadedServer.print("Machine "
//									+ deadMachine
//									+ " dead Time difference: Left: "
//									+ Math.abs(time
//											- objectRunner.previousTimeLeft)
//									+ ", right: "
//									+ Math.abs(time
//											- objectRunner.previousTimeRight));
						}

						if (objectRunner.isRightDead) {
							// because the machine who send the message
							// (right:..) would be on the left.
							workerObject.setMessageReceived(workerObject
									.formNewLeftDeadMessage());
							messageReceived = workerObject.getMessageReceived();
						}

						String[] messageReceivedArray = messageReceived
								.split(";");
						for (String message : messageReceivedArray) {
							if (message.length() > 5) {
								// String messageReceived =
								// workerObject.getMessageReceived();
								if (message.split(":")[0]
										.equalsIgnoreCase("left")) {

									if (objectRunner.isRightDead) {
//										MultiThreadedServer
//												.print("Left-Status: "
//														+ String.valueOf(objectRunner.isLeftDead)
//														+ ":"
//														+ String.valueOf(objectRunner.isRightDead));

										Message insertMessage = new Message(
												message);
										insertMessage.setCode("machineInsert");
										objectRunner
												.processMessage(insertMessage
														.formMessageNow());
									}

								} else if (message.split(":")[0]
										.equalsIgnoreCase("right")) {

									if (objectRunner.isLeftDead) {
//										MultiThreadedServer
//												.print("Right-Status: "
//														+ String.valueOf(objectRunner.isLeftDead)
//														+ ":"
//														+ String.valueOf(objectRunner.isRightDead));

										Message insertMessage = new Message(
												message);
										insertMessage.setCode("machineInsert");
										objectRunner
												.processMessage(insertMessage
														.formMessageNow());
									}

								}

								if (objectRunner.processMessage(message)) {
									MultiThreadedServer
											.print("I am almost connected");
									objectRunner.sendLetsConnectMessage();
									// break;
								}
								// if (messageReceived.length() > 5) {
								// System.out.println("Message Received: " +
								// messageReceived);
								// objectRunner.processMessage(messageReceived);
								// }
							}
						}
					}
				} catch (ConcurrentModificationException e) {
					e.printStackTrace();
				}
			}

			if (serverObject.workerList.size() == 0 
					&& ((System.currentTimeMillis() - objectRunner.previousReconnectRequest) > MIN_RECONNECT_TIME)) {
				
				if (objectRunner.reconnectRequests > 0) {
					
					MultiThreadedServer.print("Trying to connect to :" + objectRunner.tempListMachines.get(1).split("-")[0]); 
					
					objectRunner.leftClient = new Client(objectRunner.tempListMachines.get(1).split("-")[0],
							20000);
//					objectRunner.rightClient = new Client(objectRunner.getIPAddressRight(),
//							objectRunner.getServerPort());
					objectRunner.previousTimeLeft = System.currentTimeMillis();
					objectRunner.previousTimeRight = System.currentTimeMillis();
				}
				
				objectRunner.previousReconnectRequest = System.currentTimeMillis(); 
				objectRunner.processMessage(createMachineInsert());
				MultiThreadedServer.print("Trying to connect to a machine");
				objectRunner.reconnectRequests++; 
			}
		}
	}

	public String isAnybodyDead() {

		long time = System.currentTimeMillis();
		// System.out.println("Left: " + Math.abs(time - previousTimeRight));
		// System.out.println("Right: " + Math.abs(time - previousTimeLeft));

		if (Math.abs(time - previousTimeRight) > MIN_TIME_BW_PINGS) {
			this.isRightDead = true;
			// this.previousTimeRight = System.currentTimeMillis();
//			MultiThreadedServer.print("HELP. Right is disconnected.");
			return "right";
		}

		if (Math.abs(time - previousTimeLeft) > MIN_TIME_BW_PINGS) {
			this.isLeftDead = true;
			// this.previousTimeLeft = System.currentTimeMillis();
//			MultiThreadedServer.print("HELP. Left is disconnected.");
			return "left";
		}

		return "none";
	}

	public boolean processMessage(String messageCombined) {

		String[] messageReceived = messageCombined.split(";");
		for (String message : messageReceived) {
			if (message.length() < 5) {
				continue;
			}
			Message encodedMessage = new Message(message);
			String direction = message.split(":")[0];

			String messageToBeSent = encodedMessage
					.analyzeMessage(this.hashtable);

//			MultiThreadedServer.print(messageToBeSent);
			
			if (encodedMessage.getCode().equalsIgnoreCase("connectToMe")) {
				if (direction.equalsIgnoreCase("left")) {
					if (encodedMessage.getSourceIP().equalsIgnoreCase(Message.getMyIP())) {
						MultiThreadedServer.print("Our IPs are same and we're just going to finish the thing"); 
						rightClient = new Client(encodedMessage.getDestinationIP(), 20000); 
//						rightClient.sendData(encodedMessage.formMessageNow()); 
					} else {
						rightClient = new Client(encodedMessage.getSourceIP(),
								20000);
						rightClient.sendData(encodedMessage.formMessageNow());
					}
				} else if (direction.equalsIgnoreCase("right")) {
					leftClient = new Client(encodedMessage.getSourceIP(), 20000);
					leftClient.sendData(encodedMessage.formMessageNow());
				}
			} else if (encodedMessage.getCode().equalsIgnoreCase(
					"machineInsert")) {
				if (encodedMessage.getTTL() == 0) {
					encodedMessage.clearMessage();
					continue;
				} else {
					encodedMessage.setTTL(encodedMessage.getTTL() - 1);
				}

				if (!this.isLeftDead && !this.isRightDead) {
//					String rightIP = rightClient.getIpAddress();
//					int rightPort = rightClient.getPort(); // encodedMessage.createConnectionNewMachineMessage()

					// encodedMessage.setSourceIP(Message.getMyIP() + "-20000");

					leftClient.sendData("Left:"
							+ encodedMessage
									.createConnectionNewMachineMessage(tempListMachines.get(tempListMachines.size() - 1).split("-")[0]));

					rightClient.sendData("Right:"
							+ encodedMessage
									.createConnectionNewMachineMessage(tempListMachines.get(tempListMachines.size() - 1).split("-")[0]));

					// rightClient = new Client(encodedMessage.getSourceIP(),
					// encodedMessage.getSourcePort());

					// encodedMessage.setDestination(rightIP + "-20000");
					// rightClient.sendData("Right:" + messageToBeSent);
				}
			} else if (encodedMessage.getCode()
					.equalsIgnoreCase("ConnectRight")) {
				if (rightClient.isConnectionDead()) {
					rightClient = new Client(encodedMessage.getDestinationIP(),
							encodedMessage.getDestinationPort());
					encodedMessage.clearMessage();
					rightClient.sendData("Right:" + messageToBeSent);
				}
			} else if (encodedMessage.getCode().equalsIgnoreCase(
					"reconnect_left_dead")) {
				ArrayList<String> listMachines = getListMachinesDisSys(FILE_LIST_MACHINES_OF_DISSYS);
				for (String destination : listMachines) {
					// MultiThreadedServer.print("I tried to connect to: " +
					// destination);
					try {
						Client tempClient = new Client(
								destination.split("-")[0],
								Integer.parseInt(destination.split("-")[1]));
						if (tempClient.isServerAvail) {
							tempClient.destroyConnection();
							leftClient = new Client(destination.split("-")[0],
									Integer.parseInt(destination.split("-")[1]));
							rightClient = new Client(
									rightClient.getIpAddress(),
									rightClient.getPort());
							MultiThreadedServer.print("Got Connected to: "
									+ destination);
							this.isRightDead = false;
							this.isLeftDead = false;
							this.previousTimeLeft = System.currentTimeMillis();
							this.previousTimeRight = System.currentTimeMillis();
							// encodedMessage.clearMessage();
							return true;
						} else {
							MultiThreadedServer.print("Couldn't connect to: "
									+ destination);
						}
					} catch (Exception e) {
						continue;
					}
				}
			} else if (encodedMessage.getCode().equalsIgnoreCase("LetsConnect")) {
				MultiThreadedServer.print("Let's connect message came from : "
						+ encodedMessage.getSourceIP() + ":"
						+ encodedMessage.getSourcePort());
				MultiThreadedServer.print("Lets connect message received by : "
						+ Message.getMyIP());
//				MultiThreadedServer.print("Status: "
//						+ String.valueOf(isLeftDead) + ":"
//						+ String.valueOf(isRightDead));
				// MultiThreadedServer.print("Status: " +
				// String.valueOf(isLeftDead) + ":" +
				// String.valueOf(isRightDead));

				if (direction.equalsIgnoreCase("left")) {
					this.rightClient = new Client(encodedMessage.getSourceIP(),
							encodedMessage.getSourcePort());
					MultiThreadedServer.print("Connecting to right machine: "
							+ Message.getMyIP());
				} else if (direction.equalsIgnoreCase("right")) {
					this.leftClient = new Client(encodedMessage.getSourceIP(),
							encodedMessage.getSourcePort());
					MultiThreadedServer.print("Connecting to left machine: "
							+ Message.getMyIP());
				}

				if (this.isLeftDead) {
					this.rightClient = new Client(encodedMessage.getSourceIP(),
							encodedMessage.getSourcePort());
					this.leftClient = new Client(leftClient.getIpAddress(),
							leftClient.getPort());
				} else {
					this.leftClient = new Client(encodedMessage.getSourceIP(),
							encodedMessage.getSourcePort());
					this.rightClient = new Client(rightClient.getIpAddress(),
							rightClient.getPort());
				}
				this.isRightDead = false;
				this.isLeftDead = false;
				this.previousTimeLeft = System.currentTimeMillis();
				this.previousTimeRight = System.currentTimeMillis();
				encodedMessage.clearMessage();
				return false;
			} else if (encodedMessage.getCode().equalsIgnoreCase("insert")) {
				Message message_1 = new Message(messageToBeSent);
				message_1.setTTL(message_1.getTTL() - 1);
				this.sendAliveMessage(message_1.formMessageNow());

				message_1.setTTL(message_1.getTTL() + 1);
				this.sendAliveMessage(message_1.formMessageNow());

				message_1.setTTL(message_1.getTTL());
				this.sendAliveMessage(message_1.formMessageNow());

			} else {
				if (!encodedMessage.getCode().equalsIgnoreCase("")) {
					this.sendAliveMessage(messageToBeSent);
				}
			}
		}
		return false;
	}

	public ArrayList<String> getListMachinesDisSys(String filename) {
		ArrayList<String> machinesNames = new ArrayList<String>();
		try {
			String line;
			FileInputStream inFile = new FileInputStream(filename);
			DataInputStream data = new DataInputStream(inFile);
			BufferedReader br = new BufferedReader(new InputStreamReader(data));
			while ((line = br.readLine()) != null) {
				machinesNames.add(line);
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return machinesNames;
	}

	public void sendAliveMessage(String messagetoSend) {
		String messageLeft = "Left:ping:" + Message.getMyIP()
				+ "-20000:noDestination:noKey:noValue:"
				+ System.currentTimeMillis() + ":1:loading-";
		String messageRight = "Right:ping:" + Message.getMyIP()
				+ "-20000:noDestination:noKey:noValue:"
				+ System.currentTimeMillis() + ":1:loading-";

		long presentTime = System.currentTimeMillis();

		if (Math.abs(presentTime - this.previousMessageSendTime) > TIME_BW_HEARTBEATS) {
			try {
				this.previousMessageSendTime = System.currentTimeMillis();
				leftClient.sendData(messageLeft);
				rightClient.sendData(messageRight);

				leftClient.sendData("Left:" + messagetoSend);
				// rightClient.sendData("Right:" + messagetoSend);

				// leftClient.sendData("Right:" + messagetoSend);
				// rightClient.sendData("Left:" + messagetoSend);
			} catch (Exception e) {
				// MultiThreadedServer.print("Machine disconnected.");
			}
		}

		// try {
		// Thread.sleep(TIME_BW_HEARTBEATS);
		// } catch (InterruptedException e) {
		// e.printStackTrace();
		// }
	}

	public void sendLetsConnectMessage() {
		String messageLeft = "Left:LetsConnect:" + Message.getMyIP()
				+ "-20000:noDestination:noKey:noValue:"
				+ System.currentTimeMillis() + ":1:newConnection";
		String messageRight = "Right:PING:" + Message.getMyIP()
				+ "-20000:noDestination:noKey:noValue:"
				+ System.currentTimeMillis() + ":1:normalMessage";

		try {
			rightClient.sendData(messageRight);
			leftClient.sendData(messageLeft);
			// rightClient.sendData(messageLeft);
			// leftClient.sendData(messageRight);

		} catch (Exception e) {
			// MultiThreadedServer.print("Machine disconnected.");
		}

		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public String createRandomInsertMessage(int numMachines) {
		numMachines--;
		long key = (System.currentTimeMillis() % 100000);
		String str = "insert:" + Message.getMyIP() + "-20000:noDestination:"
				+ key + ":blank_value:" + System.currentTimeMillis() + ":"
				+ numMachines + ":loading-";
		return str;
	}

	public String createRandomSearchMessage(int numMachines) {
		numMachines--;
		long key = (System.currentTimeMillis() % 100000);
		String str = "search:" + Message.getMyIP() + "-20000:noDestination:"
				+ key + ":blank_value:" + System.currentTimeMillis() + ":"
				+ numMachines + ":hoppedIPs-";
		return str;
	}

	public String createRandomDeleteMessage(int numMachines) {
		numMachines--;
		long key = (System.currentTimeMillis() % 100000);
		String str = "delete:" + Message.getMyIP() + "-20000:noDestination:"
				+ key + ":blank_value:" + System.currentTimeMillis() + ":"
				+ numMachines + ":hoppedIPs-";
		return str;
	}

	public static String createMachineInsert() {
		int ttl = 2;
		long key = (System.currentTimeMillis() % 100000);
		String str = "machineInsert:" + Message.getMyIP()
				+ "-20000:noDestination:" + key + ":blank_value:"
				+ System.currentTimeMillis() + ":" + ttl + ":hoppedIPs-";
		return str;
	}

	public void sendAliveMessage() {
		// String messageLeftPing = "Left:PING:" + Message.getMyIP() +
		// "-20000:noDestination:noKey:noValue:" + System.currentTimeMillis() +
		// ":1:loading-";
		String messageRightPing = "Right:PING:" + Message.getMyIP()
				+ "-20000:noDestination:noKey:noValue:"
				+ System.currentTimeMillis() + ":1:loading-";

		String messageLeft = "Left:" + createRandomInsertMessage(NUM_MACHINES);
		String messageRight = "Right:" + createRandomInsertMessage(NUM_MACHINES);

		String messageSearchLeft = "Left:" + createRandomSearchMessage(NUM_MACHINES+1);
		String messageSearchRight = "Right:" + createRandomSearchMessage(NUM_MACHINES+1);

		String messageDeleteLeft = "Left:" + createRandomDeleteMessage(NUM_MACHINES+1);
		String messageDeleteRight = "Right:" + createRandomDeleteMessage(NUM_MACHINES+1);

		// String messageLeft = "Left:" + createRandomInsertMessage(3);
		// String messageRight = "Right:" + createRandomInsertMessage(3);

		long presentTime = System.currentTimeMillis();

		if (Math.abs(presentTime - this.previousMessageSendTime) > TIME_BW_HEARTBEATS) {
			this.previousMessageSendTime = System.currentTimeMillis();
			try {
				// rightClient.sendData(messageRightPing);
				// leftClient.sendData(messageLeftPing);

				// rightClient.sendData(messageRight);
				leftClient.sendData(messageLeft);

				// rightClient.sendData(messageSearchRight);
				leftClient.sendData(messageSearchLeft);

				// rightClient.sendData(messageDeleteRight);
				leftClient.sendData(messageDeleteLeft);

				rightClient.sendData(messageRightPing);

				// rightClient.sendData(messageLeft);
				// leftClient.sendData(messageRight);

			} catch (Exception e) {
				// MultiThreadedServer.print("Machine disconnected.");
			}
		}
		try {
			Thread.sleep(100);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	public void setIPAddresses(String iPAddressLeft, String iPAddressRight) {
		setIPAddressLeft(iPAddressLeft);
		setIPAddressRight(iPAddressRight);
	}

}
